/*/*!
 * Copyright (c) 2010 Jacob Essex
 *
 * Dual licenced under MIT or GPL 3 licneces
 * 
 * See the GPL3 or MIT files in the main directory for details
 */

/* 
 *
 * Database namespace
 */
require.def('db', ['log/db'], function(Log){
    var createdDb = {};

    dbObj = {
        ROW_NOT_CREATED : -1,

        /**
         * This class wraps some of the database functionality to try to avoid opening the database
         *
         * @param path
         * @param version
         * @param open a callback function to open a database. This is primaraly used for mocking
         */
        Lazy : function(path, version, name, size, open){
            if ( this instanceof dbObj.Lazy === false ){
                throw new Error('Lazy is not a function');
            }
            var db = undefined;
            this.transaction = function(callback){
                if ( db === undefined ){
                    Log.info('Db.Lazy: Created new database');
                    db = open !== undefined ? open(path, version) : openDatabase(path, version, name, size);
                    if ( db === undefined ){
                        throw new Error('Failed to created database');
                    }
                }
                return db.transaction(callback); 
            };
        },

        TableDef : function(name, columns){
            this.name = name;
            this.columns = columns ;
        },

        Row : function(obj, db, tbl, id){
            if ( this instanceof dbObj.Row === false ){
                throw new Error('Row is not a function');
            }
    
            //There is only a getter for this as the system cannot cope with the id
            //ever changing. I would prefer not to expose this at all but some 
            //functions need it.
            this.getId = function(){
                return id;
            }

            this.save = function(tx, funcs){
                var sqlFunc = function(tx){
                    var vals = tbl.columns.map(function(k){ return obj[k];}); //gets the values of the properties

                    if ( id !== dbObj.ROW_NOT_CREATED ){ 

                        var sql = 'UPDATE ' + tbl.name 
                                + ' SET ' + tbl.columns.map(function(k){ return k+'=?'; }).join() 
                                + ' WHERE Id = ?';

                        Log.info('Db.Row: executing ' + sql);
                        Log.info('Db.Row: values: ' + vals.reduce(function(c, v){ return c + ',' + v}, ''));
                        
                        vals.push(id); //the above query also includes the Id, which isn't pushed by default
                        
                        tx.executeSql(
                            sql,
                            vals,
                            function(){ funcs && funcs.success && funcs.success(); },
                            function(tx, e){ funcs && funcs.error && funcs.error(e.message); }
                        )
                        
                    }else{ //if the id isn't known, insert into the db and set the id

                        var sql = 'INSERT INTO ' + tbl.name + ' (' + tbl.columns.join() + ')'
                                + ' VALUES (' + tbl.columns.map(function(x){return '?';}).join() + ')';
                        Log.info('Db.Row: executing ' + sql);
                        Log.info('Db.Row: values: ' + vals.reduce(function(c, v){ return c + ',' + v}, ''));

                        tx.executeSql(sql, vals, 
                            function(tx, rs){ //wrapper around the function that was passed in to allow us to set the id
                                id = rs.insertId;
                                Log.info('Db.Row: New id is ' + id);
                                funcs && funcs.success && funcs.success();
                            },
                            function(tx, e){ 
                                funcs && funcs.error && funcs.error(e.message);
                            }
                        );
                    }
                };
                if ( tx ){
                    sqlFunc(tx);
                }else{
                    db.transaction(sqlFunc);
                }


            };
        },


        /**
         * This is a cache of databse connections to avoid opening multiple connections to the same
         * database. It isn't great and I am not convinced it is a good idea...
         */
        getDb:function(name){
            if ( createdDb[name] === undefined ){
                createdDb[name] = new dbObj.Lazy(name, '', '', 1024);
            }
            return createdDb[name];
        }

    }


    /* ******************************************************************************* */


    /**
     * This is a set of helper functions for Db objects. It allows most of the common
     * tasks to be done in one line
     */
    dbObj.site = (function(){

        var buildWhere = function(clms){
            return clms.length ? ' WHERE ' + clms.map(function(x){ return x + '=?' }).join() : '';
        };

        return{

            count : function(tx, table, clms, values, funcs){
                if ( funcs === undefined ){
                    return;
                }
                    Log.info('SELECT COUNT(*) AS C FROM ' + table + buildWhere(clms));
                tx.executeSql(
                    'SELECT COUNT(*) AS C FROM ' + table + buildWhere(clms),
                    values,
                    function(tx, rs){
                        Log.info('Count was ' + rs.rows.item(0).C);
                        funcs.success(tx, rs.rows.item(0).C);
                    },
                    function(tx, err){
                        Log.info('error');
                        funcs.error && funcs.error(err.message);
                    }
                ); 
            },

            exists : function(tx, table, clms, values, funcs){
                if ( funcs === undefined ){
                    return;
                }

                tx.executeSql(
                    'SELECT * FROM ' + table + buildWhere(clms) + ' LIMIT 1',
                    values,
                    function(tx, rs){
                        if ( rs.rows.length === 0 ){
                            funcs.no && funcs.no(tx);
                        }else{
                            funcs.yes && funcs.yes(tx) 
                        }
                    },
                    function(tx, err){ funcs.error && funcs.error(err.message) }
                ); 
            },

            getUnique:function(tx, table, clms, values, createFunc, funcs){
                if ( funcs === undefined ){
                    return;
                }

                tx.executeSql(
                    'SELECT * FROM '+table + buildWhere(clms),
                    values,
                    function(tx, rs){
                        if ( rs.rows.length == 0 ){
                            funcs.notFound && funcs.notFound();
                        }else if ( rs.rows.length > 1 ){
                            funcs.error && funcs.error('Unexpected number of results. Expceted 1 got '+rs.rows.length);
                        }else{
                            var row = rs.rows.item(0); //there should only be one
                            var id = row['Id'];
                            Log.info('Found site with Id ' + id);
                            var obj = createFunc(id); 
                            throw new Error();
                            //fix... needed hre
                            tbl.columns.forEach(function(k){ obj[k] = row[k]; }); //transfer properties
                            funcs.success && funcs.success(tx, obj);
                        }
                    },
                    function(tx, er){
                        funcs.error && funcs.error(er.message);
                    }
                );

            }
        };
    })();

    /* ******************************************************************************* */

    /* ******************************************************************************* */
    /**
     *  
     *
     */
    dbObj.chainWith = function(db, tx){

        function popFunctions(tx, curChain, successFunc, unexpectedFunc, errorFunc){

            if ( curChain.length === 0 ){
                successFunc();
                return;
            }
            var cur = curChain.pop();

            var funcArgs = {error:errorFunc};

            var pos = ['yes', 'no', 'notFound', 'success'];
            pos.forEach(function(v){
                funcArgs[v] = unexpectedFunc;//function(){ Log.warn('Unexpected state: ' + v); }
            });

            funcArgs[cur.expectedType] = function(arg){
                if ( cur.expected == undefined ){
                    popFunctions(tx,curChain, successFunc, unexpectedFunc, errorFunc);
                }else if ( typeof(cur.expected) === 'function' ){
                    if ( cur.expected.apply(null, arguments) ){
                        popFunctions(tx,curChain, successFunc, unexpectedFunc, errorFunc);
                    }
                }else if ( arg === cur.expected ){
                    popFunctions(tx,curChain, successFunc, unexpectedFunc, errorFunc);
                }
            }
            var args = [tx];
            if ( cur.args.length > 0 ){
                args = args.concat(cur.args);
            }
            args.push(funcArgs);

            Log.info('chainWith.next: ' + cur.func + '(' + args.length + ')');

            var o = new cur.obj(db);
            o[cur.func].apply(o, args);
        }


        return (function(){
            var chain = [];
            var successFunc = function(){};
            var errorFunc = function(){};
            var unexpectedFunc = function(){};
            return {
                queue:function(obj, func, args, expectedType, expected){
                    chain.push({
                        'obj':obj,
                        'func':func,
                        'args':args,
                        'expectedType':expectedType,
                        'expected':expected
                    });
                    return this;
                },
                error:function(f){
                    errorFunc = f;
                    return this;
                },
                success:function(f){
                    successFunc = f;
                    return this;
                },
                unexpected:function(f){
                    unexpectedFunc = f;
                    return this;
                },
                /**
                 * This function doesn't return this to ensure it is the last in the chain
                 */
                exec:function(){
                    var start = function(tx){
                        chain.reverse();
                        popFunctions(tx, chain, successFunc, unexpectedFunc, errorFunc);
                    }
                    if ( tx ){
                        start(tx);
                    }else{
                        db.transaction(start);
                    }
                },
                /**
                 * This is a conecpt only to allow the current transaction to be commited and a new one created
                 */
                commit:function(){}
            }
        })();
    }



    return dbObj;

});

